/*
 *
 *   Copyright (C) 2005 by Raymond Huang
 *   plushuang at users.sourceforge.net
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *  ---
 *
 *  In addition, as a special exception, the copyright holders give
 *  permission to link the code of portions of this program with the
 *  OpenSSL library under certain conditions as described in each
 *  individual source file, and distribute linked combinations
 *  including the two.
 *  You must obey the GNU Lesser General Public License in all respects
 *  for all of the code used other than OpenSSL.  If you modify
 *  file(s) with this exception, you may extend this exception to your
 *  version of the file(s), but you are not obligated to do so.  If you
 *  do not wish to do so, delete this exception statement from your
 *  version.  If you delete this exception statement from all source
 *  files in the program, then also delete it here.
 *
 */

#include <urlglib/download_task_curl.h>
#include <urlglib/url_info.h>
#include <urlglib/urlglib_util.h>
#include <urlglib/ug_i18n.h>
#include <curl/curl.h>

// DownloadTaskCurl definition ---------------------------------------

// Hierarchy :
//
// DownloadTask
// |
// `--- DownloadTaskCurl
//

typedef struct _DownloadTaskCurl    DownloadTaskCurl;

struct _DownloadTaskCurl {
	DOWNLOAD_TASK_FIELDS;

	CURL*      curl;
	GTimer*    timer;
	gboolean   resumable;
	curl_off_t resume_from;

	FILE*      file;
	gboolean   redirection;
	gboolean   redirection_error;
	char       error_string[CURL_ERROR_SIZE + 1];
};

static int  download_task_curl_run (DownloadTask* dtask);
static void download_task_curl_finalize (DownloadTask* dtask);
static int  curl_progress_func (DownloadTaskCurl* dtask_curl,
                                double dl_total, double dl_now,
                                double ul_total, double ul_now);

// global functions --------------------------------------------------
void download_task_curl_global_init ()
{
	curl_version_info_data* curl_data;

	curl_global_init (CURL_GLOBAL_ALL);
	curl_data = curl_version_info (CURLVERSION_NOW);
	download_task_add_protocols (curl_data->protocols, download_task_curl_new);
}

void download_task_curl_global_finalize ()
{
	curl_global_cleanup ();
}

// DownloadTaskCurl implement ----------------------------------------

DownloadTask*  download_task_curl_new ()
{
	DownloadTaskCurl* dtask_curl = g_malloc0 (sizeof (DownloadTaskCurl));
	DownloadTask*     dtask = (DownloadTask*)dtask_curl;

	// initialize base class
	download_task_instance_init (dtask);
	dtask->finalize = download_task_curl_finalize;
	dtask->run      = download_task_curl_run;

	dtask_curl->curl  = curl_easy_init ();
//	dtask_curl->timer = NULL;
	dtask_curl->resumable       = TRUE;
//	dtask_curl->resume_from     = 0;
//	dtask_curl->error_string[0] = 0;

	return (DownloadTask*)dtask_curl;
}

static void download_task_curl_finalize (DownloadTask* dtask)
{
	DownloadTaskCurl* dtask_curl = (DownloadTaskCurl*)dtask;

	if (dtask_curl->timer)
		g_timer_destroy (dtask_curl->timer);
	curl_easy_cleanup (dtask_curl->curl);

	download_task_instance_finalize (dtask);
}

static gdouble download_task_curl_timer_elapsed (DownloadTaskCurl* dtask_curl)
{
	if (dtask_curl->timer)
		return g_timer_elapsed (dtask_curl->timer, NULL);
	else {
		dtask_curl->timer = g_timer_new ();
		return 1.0;
	}
}

static size_t download_task_curl_header (char *buffer, size_t size, size_t nmemb, DownloadTaskCurl* dtask_curl)
{
	DownloadNode* dnode = dtask_curl->download;
	gboolean url_changed = FALSE;
	gint     total_size = size*nmemb;
	gint     offset = 0;
	UrlInfo* url_info;

	gint     line_len;
	gchar*   temp_str;
	gchar*   old_fullpath;

	do {
		line_len = ug_str_line_len (buffer, total_size, offset);

		// handle HTTP header "Location:"
		if (line_len > 10 && strncmp (buffer+offset, "Location: ", 10) == 0) {
			offset = ug_str_skip_space (buffer, total_size, offset+10);
			line_len = ug_str_line_len (buffer, total_size, offset);
			temp_str = g_strndup (buffer+offset, line_len);
			download_node_lock (dnode);
			download_node_set_url (dnode, temp_str);
			download_node_unlock (dnode);
			download_node_set_message (dnode, _("Redirection."));
			download_node_modified (dnode);
			g_free (temp_str);
			dtask_curl->redirection = TRUE;
			url_changed = TRUE;
		}
		// handle HTTP header "Content-Location:"
		if (line_len > 18 && strncmp (buffer+offset, "Content-Location: ", 18) == 0) {
			offset = ug_str_skip_space (buffer, total_size, offset+18);
			line_len = ug_str_line_len (buffer, total_size, offset);
			url_changed = TRUE;
		}

		// if header contain "Content-Location:" or "Location:"
		if (url_changed) {
			url_changed = FALSE;
			url_info = url_info_new_len (buffer+offset, line_len);
			temp_str = url_info_get_filename (url_info);
			url_info_free (url_info);

			download_node_lock (dnode);
			download_node_set_filename (dnode, temp_str);
			download_node_unlock (dnode);
			download_node_modified (dnode);
			g_free (temp_str);

			old_fullpath = g_strdup (dtask_curl->fullpath);
			if (download_task_load_fullpath (dtask_curl) == NULL) {
				dtask_curl->redirection_error = TRUE;
				download_node_set_message (dnode, _("Filename repeated or directory can't be created."));
			}
			else {
				fclose (dtask_curl->file);
				ug_rename (old_fullpath, dtask_curl->fullpath);
				// check file
				dtask_curl->file = ug_fopen (dtask_curl->fullpath, (dtask_curl->resumable)? "ab" : "wb");
				if (dtask_curl->file) {
					curl_easy_setopt (dtask_curl->curl, CURLOPT_WRITEDATA , dtask_curl->file);
				}
				else {
					dtask_curl->redirection_error = TRUE;
					download_node_set_message (dnode, _("File open failure."));
				}
			}
			g_free (old_fullpath);
		}

		offset = ug_str_next_line (buffer, total_size, offset + line_len);
	} while (offset && dtask_curl->redirection==FALSE);

	if (dtask_curl->redirection || dtask_curl->redirection_error)
		return 0;
	return nmemb;
}

static int  download_task_curl_run (DownloadTask* dtask)
{
	DownloadTaskCurl* dtask_curl = (DownloadTaskCurl*)dtask;
	DownloadNode* dnode = dtask->download;
	ProxyNode* proxy;

	CURL*    curl = dtask_curl->curl;
	CURLcode curl_code;

	char* proxy_usr_pwd = NULL;
	char* usr_pwd = NULL;
	int   result = DOWNLOAD_TASK_CAN_RETRY;
	int   n_redirection = 0;

	// check and encode URL
	if (download_task_load_url (dtask, TRUE) == FALSE) {
		download_node_set_message (dnode, _("Invalid URL."));
		result = DOWNLOAD_TASK_ERROR;
		goto exit;
	}

	// set referrer
//	referrer = g_strdup_printf ("Referrer: %s", STR_N2E (setting->referer));
//	slist = curl_slist_append (slist, referrer);
//	curl_easy_setopt (curl, CURLOPT_HTTPHEADER, slist);
	curl_easy_setopt (curl, CURLOPT_REFERER, dtask->referer);

	// check file
	dtask_curl->file = ug_fopen (dtask_curl->fullpath, (dtask_curl->resumable)? "ab" : "wb");
	if (dtask_curl->file==NULL) {
		download_node_set_message (dnode, _("File open failure."));
		result = DOWNLOAD_TASK_ERROR;
		goto exit;
	}

	if (dtask_curl->resumable) {
		ug_fseek (dtask_curl->file, 0, SEEK_END);
		dtask_curl->resume_from = ug_ftell (dtask_curl->file);
	} else {
		dtask_curl->resume_from = 0;
	}

	curl_easy_setopt (curl, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)");
//	curl_easy_setopt (curl, CURLOPT_COOKIEFILE, "");
//	curl_easy_setopt (curl, CURLOPT_UNRESTRICTED_AUTH, 1);

	download_node_lock (dnode);
	// set login id & password
	if (dnode->login) {
		usr_pwd = g_strdup_printf ("%s:%s",
		                           (dnode->username) ? dnode->username : "",
		                           (dnode->password) ? dnode->password : "");
		curl_easy_setopt (curl, CURLOPT_USERPWD, usr_pwd);
		curl_easy_setopt (curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
	} else {
		curl_easy_setopt (curl, CURLOPT_USERPWD, NULL);
	}
	download_node_unlock (dnode);

	// set url
	curl_easy_setopt (curl, CURLOPT_URL, dtask->url);

	// set download data
	curl_easy_setopt (curl, CURLOPT_WRITEDATA, dtask_curl->file);
	curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION , fwrite);
//	curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION , curl_write_func);
	curl_easy_setopt (curl, CURLOPT_NOPROGRESS, FALSE);
	curl_easy_setopt (curl, CURLOPT_PROGRESSFUNCTION, curl_progress_func);
	curl_easy_setopt (curl, CURLOPT_PROGRESSDATA, dtask_curl);
	curl_easy_setopt (curl, CURLOPT_RESUME_FROM_LARGE, dtask_curl->resume_from);
	curl_easy_setopt (curl, CURLOPT_ERRORBUFFER, dtask_curl->error_string);

	// set proxy ----------------------------------------------------
	curl_easy_setopt (curl, CURLOPT_PROXY, NULL);
	if (download_task_load_proxy (dtask)) {
		proxy = dtask->proxy;
		switch (proxy->type) {
		case PROXY_TYPE_SOCKS4:
			curl_easy_setopt (curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS4);
			break;
		case PROXY_TYPE_SOCKS5:
			curl_easy_setopt (curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
			break;
		case PROXY_TYPE_HTTP:
		default:
			curl_easy_setopt (curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
		}
		curl_easy_setopt (curl, CURLOPT_PROXY, proxy->host);
		curl_easy_setopt (curl, CURLOPT_PROXYPORT, proxy->port);
		if (proxy->authentication ||
		    proxy->type == PROXY_TYPE_SOCKS4 ||
			proxy->type == PROXY_TYPE_SOCKS5)
		{
			proxy_usr_pwd = g_strdup_printf ("%s:%s",
			                                 (proxy->username) ? proxy->username : "",
			                                 (proxy->password) ? proxy->password : "");
			curl_easy_setopt (curl, CURLOPT_PROXYUSERPWD, proxy_usr_pwd);
		}
	}

	// set redirection & handle header
	curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, 0);
	curl_easy_setopt (curl, CURLOPT_HEADERFUNCTION, download_task_curl_header);
	curl_easy_setopt (curl, CURLOPT_HEADERDATA, dtask_curl);

	// FTP option
	// Set CURLOPT_FTP_USE_EPSV=0 to improve FTP compatibility.
	// This will disable "Extended Passive Mode" and use "Passive Mode".
	curl_easy_setopt (curl, CURLOPT_FTP_USE_EPSV, 0);

	do {
//		printf ("download_task_curl : before perform\n");
		curl_code = curl_easy_perform (curl);
//		printf ("download_task_curl : after perform\n");

		if (dtask_curl->redirection_error) {
			dtask_curl->redirection_error = FALSE;
			dtask_curl->redirection = FALSE;
			result = DOWNLOAD_TASK_ERROR;
			goto exit;
		}
		else if (dtask_curl->redirection) {
			dtask_curl->redirection = FALSE;
			n_redirection++;
			// check and encode URL
			if (download_task_load_url (dtask, TRUE) == FALSE) {
				download_node_set_message (dnode, _("Invalid URL."));
				result = DOWNLOAD_TASK_ERROR;
				goto exit;
			}
			curl_easy_setopt (curl, CURLOPT_URL, dtask->url);
			curl_easy_setopt (curl, CURLOPT_REFERER, dtask->referer);
		}
		else {
			break;
		}
	} while (n_redirection < dnode->redirect_max);

	if (curl_code == CURLE_OK) {
		if (dnode->completed_size == dnode->total_size && dnode->completed_size) {
			result = DOWNLOAD_TASK_OK;
			goto exit;
		}
		else {
			download_node_set_message (dnode, _("File length error. Try to increase max redirection."));
			result = DOWNLOAD_TASK_ERROR;
			goto exit;
		}
	} else {
		download_node_set_message (dnode, dtask_curl->error_string);
	}

//	printf ("Curl error code %.2u : %s\n", curl_code, dtask_curl->error_string);
	// check curl return code
	switch (curl_code) {
		// can resume
		case CURLE_PARTIAL_FILE:
		case CURLE_GOT_NOTHING:
		case CURLE_OPERATION_TIMEOUTED:
		case CURLE_RECV_ERROR:
		case CURLE_BAD_CONTENT_ENCODING:
			dtask_curl->resumable = TRUE;
			break;

		// can't resume
		case CURLE_HTTP_RANGE_ERROR:
		case CURLE_BAD_DOWNLOAD_RESUME:
			dtask_curl->resumable = FALSE;
			dtask_curl->resume_from = 0;
			break;

		case CURLE_COULDNT_RESOLVE_HOST:
		case CURLE_OUT_OF_MEMORY:
		case CURLE_WRITE_ERROR:
		case CURLE_ABORTED_BY_CALLBACK:
		case CURLE_COULDNT_CONNECT:
			result = DOWNLOAD_TASK_ABORT;
			break;

		case CURLE_COULDNT_RESOLVE_PROXY:
			result = DOWNLOAD_TASK_ERROR_PROXY;
			break;

		case CURLE_UNSUPPORTED_PROTOCOL:
		case CURLE_FAILED_INIT:
		case CURLE_URL_MALFORMAT:
		case CURLE_URL_MALFORMAT_USER:
		case CURLE_FTP_WEIRD_SERVER_REPLY:
		case CURLE_FTP_ACCESS_DENIED:
		case CURLE_FTP_USER_PASSWORD_INCORRECT:
		default:
			result = DOWNLOAD_TASK_ERROR;
	}

exit:
	g_free (proxy_usr_pwd);
	g_free (usr_pwd);
	if (dtask_curl->file)
		fclose (dtask_curl->file);

	return result;
}

static int  curl_progress_func (DownloadTaskCurl* dtask_curl,
                                double dl_total, double dl_now,
                                double ul_total, double ul_now)
{
	DownloadNode* dnode = dtask_curl->download;
	gdouble timer = download_task_curl_timer_elapsed (dtask_curl);

	if (dl_now) {
		dnode->completed_size  = dl_now + dtask_curl->resume_from;
		dnode->total_size      = dl_total + dtask_curl->resume_from;
		// curl pass dl_total=0 sometimes.
		if (dnode->total_size < dnode->completed_size)
			dnode->total_size = dnode->completed_size;
		// avoid div zero, check timer
		if (timer)
			dnode->speed = dl_now / timer;
		// avoid div zero, check dnode->total_size
		if (dnode->total_size)
			dnode->percent = (dnode->completed_size / dnode->total_size)*100;
		// data store in DownloadNode was modified
		download_node_modified (dnode);
	}
	// return true when abort.
	return (dnode->state != DOWNLOAD_STATE_ACTIVE);
}
